!======================================================================
!
!    HEEDS (Higher Education Enrollment Decision Support) - A program
!      to create enrollment scenarios for 'next term' in a university
!    Copyright (C) 2012, 2013 Ricolindo L Carino
!
!    This file is part of the HEEDS program.
!
!    HEEDS is free software: you can redistribute it and/or modify
!    it under the terms of the GNU General Public License as published by
!    the Free Software Foundation, either version 3 of the License, or
!    (at your option) any later version.
!
!    HEEDS is distributed in the hope that it will be useful,
!    but WITHOUT ANY WARRANTY; without even the implied warranty of
!    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
!    GNU General Public License for more details.
!
!    You should have received a copy of the GNU General Public License
!    along with this program (see the file COPYING.TXT). If not, see
!    <http://www.gnu.org/licenses/>.
!
!    Send inquiries about HEEDS to:
!      Ricolindo L Carino
!      E-mail: Ricolindo.Carino@AcademicForecasts.com
!      Address: 600 Cruise St., Starkville, MS 39759, U.S.A.
!
!======================================================================


module CURRICULA

    use SUBJECTS

    implicit none

    ! curriculum variables
    integer, parameter :: &
    MAX_ALL_CURRICULA = 250, & ! max no. of active curricula
    MAX_LEN_CURRICULUM_CODE = 20, & ! max length of curriculum codes
    MAX_LEN_CURRICULUM_NAME = 127, & ! max length of curriculum names
    MAX_SUBJECTS_IN_CURRICULUM = 200 ! max no. of subjects in a curriculum

    integer, parameter :: &
        MAX_SUBJECTS_PER_TERM = 2*MAX_SUBJECTS_IN_CURRICULUM/3 ! max no. of subjects per term

    type :: TYPE_CURRICULUM
        logical :: Active
        character (len=MAX_LEN_CURRICULUM_CODE) :: Code
        character (len=MAX_LEN_CURRICULUM_NAME) :: Title
        character (len=MAX_LEN_CURRICULUM_NAME) :: Specialization
        character (len=MAX_LEN_CURRICULUM_NAME) :: Remark
        integer :: CollegeIdx, NumTerms, NSubjects
        integer, dimension(MAX_SUBJECTS_IN_CURRICULUM) :: SubjectIdx, SubjectTerm
    end type TYPE_CURRICULUM

    type (TYPE_CURRICULUM), dimension (0:MAX_ALL_CURRICULA) :: Curriculum
    integer :: NumCurricula

    ! freshman intake by curriculum
    integer, dimension(0:MAX_ALL_CURRICULA) :: NFintake

    ! flags to indicate processing is done for a curriculum group
    logical :: done(0:MAX_ALL_CURRICULA)

    ! file unit numbers based on generic curriculum
    integer, dimension(0:MAX_ALL_CURRICULA) :: CurrProgNum
    character (len=MAX_LEN_CURRICULUM_CODE), dimension(0:MAX_ALL_CURRICULA) :: CurrProgCode

    integer, dimension(0:20) :: TermUnits, SpecifiedUnits

    ! private tokens
    character (len=MAX_LEN_FILE_PATH), private :: fileName
    character (len=MAX_LEN_XML_LINE), private :: line
    integer, private :: unitNo=2, eof, ndels, pos(30)

contains


#include "custom_read_curricula.F90"


    subroutine xml_write_curricula(path)

        character(len=*), intent(in) :: path

        integer :: idxCOLL, idxCURR, idx, tdx, Year, Term
        character(len=255) :: mesg
        character(len=MAX_LEN_CURRICULUM_CODE) :: tCurriculum

        ! training only?
        if (noWrites) return

        fileName = trim(dirXML)//trim(path)//'CURRICULA.XML'
        call xml_open_file(unitNo, XML_ROOT_CURRICULA, fileName, idx)

        write(unitNo,AFORMAT) &
        '    <comment>', &
        '        Generated by '//PROGNAME//VERSION//' on '//currentDate(1:4)// &
                    FSLASH//currentDate(5:6)//FSLASH//currentDate(7:8), &
        '        Code - code for the curriculum', &
        '        Title - long name of curriculum', &
        '        Specialization - curriculum option/major area', &
        '        Remark - other info on the curriculum', &
        '        College - responsible college', &
        '        Status - to indicate status of curriculum: ACTIVE or INACTIVE', &
        '        Load, Year, Term, Subjects - Subjects to be taken during (Year, Term)', &
        '            where Year is one of FIRST, SECOND, THIRD, FOURTH, FIFTH', &
        '            and   Term is one of FIRST, SECOND, SUMMER', &
        '        Substitution - to indicate allowed automatic substitutions in this curriculum', &
        '            subjects in this list which are required in the curriculum will', &
        '            be considered "passed" if the rest of the subjects in the list have passing grades;', &
        '            i.e., MATH 11 and MATH 14 can be substituted by MATH I and MATH 17', &
        '    </comment>'

        do idxCURR=1,NumCurricula

            idxCOLL = Curriculum(idxCURR)%CollegeIdx

            tCurriculum = Curriculum(idxCURR)%Code
            call xml_write_character(unitNo, indent0, 'Curriculum')
            call xml_write_character(unitNo, indent1, 'Code', tCurriculum)
            call xml_write_character(unitNo, indent1, 'Title', Curriculum(idxCURR)%Title)

            if (Curriculum(idxCURR)%Specialization/=SPACE) then
                mesg = Curriculum(idxCURR)%Specialization
            else
                mesg = '(Specialization not specified)'
            end if
            call xml_write_character(unitNo, indent1, 'Specialization', mesg)

            if (Curriculum(idxCURR)%Remark/=SPACE) then
                mesg = Curriculum(idxCURR)%Remark
            else
                mesg = '(No remark)'
            end if
            call xml_write_character(unitNo, indent1, 'Remark', mesg)

            if (Curriculum(idxCURR)%Active) then
                mesg = 'ACTIVE'
            else
                mesg = 'INACTIVE'
            end if
            call xml_write_character(unitNo, indent1, 'Status', mesg)

            call xml_write_character(unitNo, indent1, 'College', College(idxCOLL)%Code)

            do tdx=1,Curriculum(idxCURR)%NumTerms
                Year = tdx/3+1
                Term = mod(tdx,3)
                if (Term == 0) Year = Year-1
                mesg = SPACE
                do idx=1,Curriculum(idxCURR)%NSubjects
                    if (Curriculum(idxCURR)%SubjectTerm(idx) == tdx) then
                        mesg = trim(mesg)//COMMA//Subject(Curriculum(idxCURR)%SubjectIdx(idx))%Name
                    end if
                end do
                if (mesg==SPACE) cycle
                call xml_write_character(unitNo, indent1, 'Load')
                call xml_write_character(unitNo, indent2, 'Year', txtYear(Year))
                call xml_write_character(unitNo, indent2, 'Term', txtSemester(Term))
                call xml_write_character(unitNo, indent2, 'Subjects', mesg(2:))
                call xml_write_character(unitNo, indent1, '/Load')
            end do
            do tdx=1,NumSubst
                if (Substitution(SubstIdx(tdx))==idxCURR) then
                    mesg = SPACE
                    do idx=SubstIdx(tdx)+1, SubstIdx(tdx+1)-1
                        mesg = trim(mesg)//COMMA//Subject(Substitution(idx))%Name
                    end do
                    call xml_write_character(unitNo, indent1, 'Substitution', mesg(2:))
                end if
            end do

            call xml_write_character(unitNo, indent0, '/Curriculum')

        end do
        call xml_close_file(unitNo, XML_ROOT_CURRICULA)

        return
    end subroutine xml_write_curricula


    subroutine xml_read_curricula(path, errNo)

        character(len=*), intent(in) :: path
        integer, intent (out) :: errNo

        character(len=MAX_LEN_XML_LINE) :: value
        character(len=MAX_LEN_XML_TAG) :: tag
        type(TYPE_CURRICULUM) :: tmpCurriculum

        integer :: i, j, k, idxterm, year, term, ierr
        character (len=MAX_LEN_SUBJECT_CODE) :: token
        character (len=MAX_LEN_COLLEGE_CODE) :: tCollege
        character (len=MAX_LEN_TEXT_SEMESTER) :: strTerm
        character (len=MAX_LEN_TEXT_YEAR) :: strYear
        integer :: nLoad, loadArray(MAX_SUBJECTS_PER_TERM)

        ! open file, return on any error
        fileName = trim(dirXML)//trim(path)//'CURRICULA.XML'
        call xml_open_file(unitNo, XML_ROOT_CURRICULA, fileName, errNo, forReading)
        if (errNo/=0) return

        ! examine the file line by line
        do
            read(unitNo, AFORMAT, iostat=eof) line
            if (eof<0) exit
            ! get tag and value if any
            call xml_parse_line(line, tag, value, eof)
            if (eof/=0) exit

            select case (trim(tag))

                case ('Curriculum') ! initialize temporary curriculum data
                    tmpCurriculum = Curriculum(0)

                case ('Code')
                    tmpCurriculum%Code = adjustl(value)

                case ('Title')
                    tmpCurriculum%Title = adjustl(value)

                case ('Specialization')
                    tmpCurriculum%Specialization = adjustl(value)

                case ('Remark')
                    tmpCurriculum%Remark = adjustl(value)

                case ('Status') ! value is ACTIVE or INACTIVE
                    tmpCurriculum%Active = trim(value)=='ACTIVE'

                case ('College') ! value is a college code
                    tCollege = value
                    tmpCurriculum%CollegeIdx = index_to_college(tCollege)

                case ('Load') ! value is empty
                    nLoad = 0
                    loadArray = 0
                    year = -1
                    term = -1

                case ('Year') ! value is one of FIRST, SECOND, THIRD, FOURTH, ...
                    strYear = adjustl(value)
                    call upper_case(strYear)
                    year = index_to_year(strYear)

                case ('Term') ! value is one of FIRST, SECOND, SUMMER
                    strTerm = adjustl(value)
                    call upper_case(strTerm)
                    term = index_to_term(strTerm)

                case ('Subjects') ! value is comma-separated list of subjects
                    call tokenize_subjects(value, ',', MAX_SUBJECTS_PER_TERM, nLoad, loadArray, ierr)

                case ('/Load') ! value is empty
                    if (term==0) term = 3 ! SUMMER
                    if (year>0 .and. term>0) then ! ok
                        ! collect subjects
                        idxTerm = (year-1)*3 + term
                        tmpCurriculum%NumTerms = idxTerm
                        do k = 1,nLoad
                            token = Subject(loadArray(k))%Name
                            i = index_to_new_subject(loadArray(k))
                            !if (i/=loadArray(k)) &
                            !    call file_log_message (tCurriculum//token//' renamed '//Subject(i)%Name)
                            if (is_offered(i,mod(term,3)) ) then
                                j = tmpCurriculum%NSubjects+1
                                tmpCurriculum%NSubjects = j
                                tmpCurriculum%SubjectIdx(j) = i
                                tmpCurriculum%SubjectTerm(j) = idxTerm
                            else
                                call file_log_message (token//': subject not offered during '//strTerm//' Term')
                            !return
                            end if
                        end do
                    end if
                    nLoad = 0
                    loadArray = 0
                    year = -1
                    term = -1

                case ('/Curriculum') ! add temporary curriculum data to Curriculum()
                    NumCurricula = NumCurricula + 1
                    call check_array_bound (NumCurricula, MAX_ALL_CURRICULA, 'MAX_ALL_CURRICULA')
                    Curriculum(NumCurricula) = tmpCurriculum
                    College(tmpCurriculum%CollegeIdx)%hasInfo = .true.

                    ! if active curriculum, check that prerequisites are taken before successor subjects
                    if (tmpCurriculum%Active) then
                        nLoad = tmpCurriculum%NSubjects
                    else
                        nLoad = 0
                        write(*,*) 'Not active: '//tmpCurriculum%Code
                    end if
                    do k = 1, nLoad
                        i = tmpCurriculum%SubjectIdx(k)
                        if (.not. is_prerequisite_satisfiable_in_curriculum(i,NumCurricula)) then
                            token = Subject(i)%Name
                            call rank_to_year_term (tmpCurriculum%SubjectTerm(k), year, term)
                            strYear = txtYear(year)
                            strTerm = txtSemester(term)
                            call file_log_message (trim(tmpCurriculum%Code)//', '// &
                            trim(strYear)//' year, '//trim(strTerm)//' term, '//trim(token)// &
                            ': preq '//trim(text_prerequisite_in_curriculum(i))//' not specified earlier!')
                        end if
                    end do
                case default
                    ! do nothing
            end select

        end do

        call xml_close_file(unitNo)

        return
    end subroutine xml_read_curricula


    subroutine xml_write_equivalencies(path)

        character(len=*), intent(in) :: path
        integer :: idx, tdx
        character(len=255) :: mesg

        ! training only?
        if (noWrites) return

        fileName = trim(dirXML)//trim(path)//'EQUIVALENCIES.XML'
        call xml_open_file(unitNo, XML_ROOT_EQUIVALENCIES, fileName, idx)
        write(unitNo,AFORMAT) &
        '    <comment>', &
        '        Generated by '//PROGNAME//VERSION//' on '//currentDate(1:4)// &
                    FSLASH//currentDate(5:6)//FSLASH//currentDate(7:8), &
        '        Equivalence - to indicate allowed automatic substitutions in all curricula', &
        '            Subjects in a list which are required in a curriculum will', &
        '            be considered "passed" if the rest of the subjects in the list have earned credits', &
        '    </comment>'
        do tdx=1,NumSubst
            if (Substitution(SubstIdx(tdx))>NumCurricula) then
                mesg = SPACE
                do idx=SubstIdx(tdx)+1, SubstIdx(tdx+1)-1
                    mesg = trim(mesg)//COMMA//Subject(Substitution(idx))%Name
                end do
                call xml_write_character(unitNo, indent0, 'Equivalence', mesg(2:))
            end if
        end do

        call xml_close_file(unitNo, XML_ROOT_EQUIVALENCIES)

        return
    end subroutine xml_write_equivalencies


    subroutine xml_read_equivalencies(path, errNo)

        character(len=*), intent(in) :: path
        integer, intent (out) :: errNo

        character(len=MAX_LEN_XML_LINE) :: value
        character(len=MAX_LEN_XML_TAG) :: tag
        character (len=MAX_LEN_CURRICULUM_CODE) :: tCurriculum
        integer :: idxCURR

        integer :: i, ptrS
        integer :: nLoad, loadArray(MAX_SUBJECTS_PER_TERM)

        ptrS = 0 ! substitutions

        ! open CURRICULUM file, return on any error
        fileName = trim(dirXML)//trim(path)//'CURRICULA.XML'
        call xml_open_file(unitNo, XML_ROOT_CURRICULA, fileName, errNo, forReading)
        if (errNo/=0) return

        ! examine the file line by line
        do
            read(unitNo, AFORMAT, iostat=eof) line
            if (eof<0) exit
            ! get tag and value if any
            call xml_parse_line(line, tag, value, eof)
            if (eof/=0) exit

            select case (trim(tag))

                case ('Code')
                    tCurriculum = adjustl(value)
                    idxCURR = index_to_curriculum(tCurriculum)

                case ('Substitution') ! value is comma-separated list of subjects
                    call tokenize_subjects(value, ',', MAX_SUBJECTS_PER_TERM, nLoad, loadArray, errNo)
                    NumSubst = NumSubst + 1
                    ptrS = ptrS+1
                    SubstIdx(NumSubst) = ptrS
                    Substitution(ptrS) = idxCURR
                    do i=1,nLoad
                        ptrS = ptrS+1
                        Substitution(ptrS) = loadArray(i)
                    end do
                case default
                    ! do nothing
            end select
        end do
        call xml_close_file(unitNo)

        ! open EQUIVALENCIES file, return on any error
        fileName = trim(dirXML)//trim(path)//'EQUIVALENCIES.XML'
        call xml_open_file(unitNo, XML_ROOT_EQUIVALENCIES, fileName, errNo, forReading)
        if (errNo/=0) return

        ! examine the file line by line
        do
            read(unitNo, AFORMAT, iostat=eof) line
            if (eof<0) exit
            ! get tag and value if any
            call xml_parse_line(line, tag, value, eof)
            if (eof/=0) exit

            select case (trim(tag))

                case ('Equivalence') ! value is comma-separated list of subjects
                    call tokenize_subjects(value, ',', MAX_SUBJECTS_PER_TERM, nLoad, loadArray, errNo)
                    NumSubst = NumSubst + 1
                    ptrS = ptrS+1
                    SubstIdx(NumSubst) = ptrS
                    Substitution(ptrS) = NumCurricula+2 ! Curriculum(NumCurricula+1) is OTHER
                    do i=1,nLoad
                        ptrS = ptrS+1
                        Substitution(ptrS) = loadArray(i)
                    end do
                case default
                    ! do nothing
            end select
        end do
        SubstIdx(NumSubst+1) = ptrS+1
        call xml_close_file(unitNo)

        return
    end subroutine xml_read_equivalencies


    subroutine read_curricula(path, errNo)

        character(len=*), intent(in) :: path
        integer, intent(out) :: errNo

        logical :: noXML
        !integer :: i, j

        ! initialize
        NumCurricula = 0
        Curriculum = TYPE_CURRICULUM (.true., SPACE, SPACE, SPACE, SPACE, 0, 0, 0, 0, 0)
        NumSubst = 0
        SubstIdx = 0
        Substitution = 0

        ! read the curriculum file
        noXML = .false.
        call xml_read_curricula(path, errNo) ! try the XML file
        if (errNo/=0) then ! something wrong with XML file
            noXML = .true.
            call  custom_read_curricula(path, errNo) ! try custom format
            if (errNo/=0) return ! something still wrong
        else
            call xml_read_equivalencies(path, errNo)
            errNo = 0
        end if
!        write(*,*) 'EQUIVALENCIES'
!        do i=1,NumSubst
!            if (Substitution(SubstIdx(i))<=NumCurricula) then
!                write(*,*) trim(Curriculum(Substitution(SubstIdx(i)))%Code)//' : ', &
!                    (trim(Subject(Substitution(j))%Name)//SPACE, &
!                    j=SubstIdx(i)+1, SubstIdx(i+1)-1)
!            else
!                write(*,*) 'All curricula  ', &
!                    (trim(Subject(Substitution(j))%Name)//SPACE, &
!                    j=SubstIdx(i)+1, SubstIdx(i+1)-1)
!            end if
!            !call Pause()
!        end do

        ! write the XML CURRICULA file?
        if (noXML .and. NumCurricula>0) then
            call xml_write_curricula(path)
            call xml_write_equivalencies(path)
        end if

        ! add OTHER
        NumCurricula = NumCurricula + 1
        Curriculum(NumCurricula)%Code = 'OTHER'
        Curriculum(NumCurricula)%CollegeIdx = NumColleges
        Curriculum(NumCurricula)%Title = 'Change curriculum'
        Curriculum(NumCurricula)%Specialization = SPACE
        Curriculum(NumCurricula)%Remark = SPACE
        Curriculum(NumCurricula)%NSubjects = 0
        Curriculum(NumCurricula)%Active = .true.

        !write(*,*) NumCurricula, ' curricula, including other...'

        call make_curriculum_groups()

        return
    end subroutine read_curricula


    subroutine make_curriculum_groups
        ! associate each curriculum with a file unit number
        integer :: i, j, k, l, idx
        CurrProgNum = 0
        CurrProgCode = SPACE
        k = 0
        do idx=1,NumCurricula
            if (CurrProgNum(idx)==0) then
                k = k+1
                CurrProgNum(idx) = k
                l = len_trim(Curriculum(idx)%Code)
                j = index(Curriculum(idx)%Code, '-')
                if (j==0) then
                    j = l
                else
                    j = j-1
                end if
                CurrProgCode(idx) = Curriculum(idx)%Code(1:j)
                do i = idx+1,NumCurricula
                    if (Curriculum(idx)%CollegeIdx==Curriculum(i)%CollegeIdx .and. &
                    Curriculum(idx)%Code(1:j)==Curriculum(i)%Code(1:j) .and. &
                    (Curriculum(i)%Code(j+1:j+1)==SPACE .or. &
                    Curriculum(i)%Code(j+1:j+1)=='-')) then
                        CurrProgNum(i) = k
                        CurrProgCode(i) = CurrProgCode(idx)
                    end if
                end do
            end if
        end do
        CurrProgNum(0) = k
        return
    end subroutine make_curriculum_groups


    function index_to_curriculum (token)
        integer :: index_to_curriculum
        character (len=MAX_LEN_CURRICULUM_CODE), intent (in) :: token
        integer :: i, j, k
        character (len=MAX_LEN_CURRICULUM_CODE) :: tCurriculum

        ! find an exact match
        tCurriculum = token
        index_to_curriculum = 0
        do i=1,NumCurricula
            if (tCurriculum==Curriculum(i)%Code) then
                index_to_curriculum = i
                return
            end if
        end do

        ! try removing the qualifiers
        do
            j = 0
            do k=len_trim(tCurriculum), 2,-1
                if (tCurriculum(k:k)=='-') then
                    j = k
                    exit
                end if
            end do
            if (j>0) then
                tCurriculum(j:) = SPACE
                do i=1,NumCurricula
                    if (tCurriculum==Curriculum(i)%Code) then
                        index_to_curriculum = -i
                        return
                    end if
                end do
            else
                exit
            end if
        end do

        ! find last code whose first few characters (up to the '-') match
        tCurriculum = trim(token)//DASH
        do i=1,NumCurricula
            if (index(Curriculum(i)%Code,trim(tCurriculum))==1) then
                index_to_curriculum = -i
            end if
        end do
        return
    end function index_to_curriculum


    function index_of_subject_in_curriculum (tCurriculum, crse)
        ! returns the first occurence of crse in tCurriculum
        integer :: index_of_subject_in_curriculum
        type (TYPE_CURRICULUM), intent (in) :: tCurriculum
        integer, intent (in) :: crse
        integer :: i
        index_of_subject_in_curriculum = 0
        do i=1,tCurriculum%NSubjects
            if (tCurriculum%SubjectIdx(i)==crse .and. &
            tCurriculum%SubjectTerm(i)>0 ) then
                index_of_subject_in_curriculum = i
                exit
            end if
        end do
        return
    end function index_of_subject_in_curriculum


    function is_used_in_college_subject (college_idx, subject_idx)
        ! returns true if subject area is used in a curriculum in college
        logical :: is_used_in_college_subject, found
        integer, intent (in) :: college_idx, subject_idx
        integer :: i
        found = .false.
        do i=1,NumCurricula
            if (Curriculum(i)%CollegeIdx/=college_idx) cycle
            if (index_of_subject_in_curriculum(Curriculum(i), subject_idx)>0) then
                found = .true.
                exit
            end if
        end do
        is_used_in_college_subject = found
        return
    end function is_used_in_college_subject


    function is_used_in_curriculum_subject_area (tCurriculum, area)
        ! returns true if subject area is in curriculum
        logical :: is_used_in_curriculum_subject_area, found
        type (TYPE_CURRICULUM), intent (in) :: tCurriculum
        character(len=*) :: area
        integer :: i
        found = .false.
        do i=1,tCurriculum%NSubjects
            if (index(Subject(tCurriculum%SubjectIdx(i))%Name,area)==1) then
                found = .true.
                exit
            end if
        end do
        is_used_in_curriculum_subject_area = found
        return
    end function is_used_in_curriculum_subject_area


    function text_prerequisite_in_curriculum(crse,tCurriculum)
        ! returns the text representation of the prerequisite of crse
        character (len=255) :: text_prerequisite_in_curriculum, displayStr
        integer, intent (in) :: crse
        type (TYPE_CURRICULUM), intent (in), optional :: tCurriculum
        character (len=MAX_LEN_SUBJECT_CODE) :: tSubject
        character (len=255) :: str127(MAX_ALL_SUBJECT_PREREQ)
        integer :: i, j, k
        ! corequisite
        displayStr = SPACE
        str127 = SPACE
        do j=Subject(crse)%lenCoreq,1,-1
            tSubject = Subject(Subject(crse)%Corequisite(j))%Name
            if (tSubject=='AND' .or. tSubject=='OR') then
                if (j/=1) then
                    str127(j) = '('//trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))//')'
                    str127(j+1) = str127(j+3)
                    str127(j+2) = SPACE
                else
                    str127(j) = trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))
                end if
            else
                str127(j) = tSubject
            end if
        end do
        if (str127(1)/='NONE' .and. str127(1)/=SPACE) displayStr = 'Co-req: '//trim(str127(1))
        ! concurrencies
        str127 = SPACE
        do j=Subject(crse)%lenConc,1,-1
            tSubject = Subject(Subject(crse)%Concurrent(j))%Name
            if (tSubject=='AND' .or. tSubject=='OR') then
                if (j/=1) then
                    str127(j) = '('//trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))//')'
                    str127(j+1) = str127(j+3)
                    str127(j+2) = SPACE
                else
                    str127(j) = trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))
                end if
            else
                str127(j) = tSubject
            end if
        end do
        if (str127(1)/='NONE' .and. str127(1)/=SPACE) then
            if (displayStr/=SPACE) then
                displayStr = 'Conc. with: '//trim(str127(1))//'. '//displayStr
            else
                displayStr = 'Conc. with: '//str127(1)
            end if
        end if
        str127 = SPACE
        do j=Subject(crse)%lenConcPreq,1,-1
            tSubject = Subject(Subject(crse)%ConcPrerequisite(j))%Name
            if (tSubject=='AND' .or. tSubject=='OR') then
                if (j/=1) then
                    str127(j) = '('//trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))//')'
                    str127(j+1) = str127(j+3)
                    str127(j+2) = SPACE
                else
                    str127(j) = trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))
                end if
            else
                str127(j) = tSubject
            end if
        end do
        if (str127(1)/='NONE' .and. str127(1)/=SPACE) then
            if (displayStr/=SPACE) then
                displayStr = 'Conc. preq: '//trim(str127(1))//'. '//displayStr
            else
                displayStr = 'Conc. preq: '//str127(1)
            end if
        end if
        str127 = SPACE
        if (present(tCurriculum)) then
            do j=Subject(crse)%lenPreq,1,-1
                k = Subject(crse)%Prerequisite(j)
                tSubject = Subject(k)%Name
                if (tSubject=='AND') then
                    if (j/=1) then
                        str127(j) = trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))
                        str127(j+1) = str127(j+3)
                        str127(j+2) = SPACE
                    else
                        str127(j) = trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))
                    end if
                else if (tSubject=='OR') then
                    if (index(str127(j+2),'*')>0) then
                        str127(j) = str127(j+1)
                    else if (index(str127(j+1),'*')>0) then
                        str127(j) = str127(j+2)
                    else
                        str127(j) = trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))
                    end if
                    if (j/=1) then
                        str127(j+1) = str127(j+3)
                        str127(j+2) = SPACE
                    end if
                else
                    if (k>0) then
                        i = index_of_subject_in_curriculum(tCurriculum, k)
                        if (i>0) then
                            str127(j) = tSubject
                        else
                            str127(j) = trim(tSubject)//'*'
                        end if
                    else
                        str127(j) = tSubject
                    end if
                end if
            end do
        else
            do j=Subject(crse)%lenPreq,1,-1
                tSubject = Subject(Subject(crse)%Prerequisite(j))%Name
                if (tSubject=='AND' .or. tSubject=='OR') then
                    if (j/=1) then
                        str127(j) = '('//trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))//')'
                        str127(j+1) = str127(j+3)
                        str127(j+2) = SPACE
                    else
                        str127(j) = trim(str127(j+2))//SPACE//trim(tSubject)//SPACE//trim(str127(j+1))
                    end if
                else
                    str127(j) = tSubject
                end if
            end do
        end if
        if (str127(1)/=SPACE) then
            if (displayStr/=' ') then
                displayStr = trim(str127(1))//'. '//displayStr
            else
                displayStr = str127(1)
            end if
        end if
        text_prerequisite_in_curriculum = displayStr
        return
    end function text_prerequisite_in_curriculum



    function is_prerequisite_satisfiable_in_curriculum(crse, curr)
        logical :: is_prerequisite_satisfiable_in_curriculum
        integer, intent (in) :: crse, curr
        integer, dimension(0:MAX_ALL_SUBJECT_PREREQ) :: tmpPreq
        integer :: ntokens, k, j, jdx, l
        character (len=MAX_LEN_SUBJECT_CODE) :: tSubject

        ! evaluate prerequisite
        tmpPreq = 0
        ntokens = Subject(crse)%lenPreq
        tmpPreq(1:ntokens) = Subject(crse)%Prerequisite(1:ntokens)
        if (crse<0) then
            tmpPreq(1) = 1
            ntokens = 0
        end if
        l = index_of_subject_in_curriculum(Curriculum(curr),crse)
        do j = ntokens,1,-1
            jdx = tmpPreq(j)
            tSubject = Subject(jdx)%Name
            if (jdx>0) then ! a named subject
                k = index_of_subject_in_curriculum(Curriculum(curr), jdx)
                if (k>0) then ! found in curriculum
                    ! check if taken earlier
                    if (Curriculum(curr)%SubjectTerm(k)<Curriculum(curr)%SubjectTerm(l)) then
                        tmpPreq(j) = 1
                    else
                        tmpPreq(j) = 0
                    end if
                else
                    tmpPreq(j) = 0
                end if
            else
                if (tSubject=='OR') then
                    tmpPreq(j) = tmpPreq(j+1) + tmpPreq(j+2)
                    tmpPreq(j+1) = 0
                    tmpPreq(j+2) = 0
                    do k=j+3,ntokens
                        tmpPreq(k-2) = tmpPreq(k)
                    end do
                else if (tSubject=='AND') then
                    tmpPreq(j) = tmpPreq(j+1) * tmpPreq(j+2)
                    tmpPreq(j+1) = 0
                    tmpPreq(j+2) = 0
                    do k=j+3,ntokens
                        tmpPreq(k-2) = tmpPreq(k)
                    end do
                else ! any other dummy subject
                    tmpPreq(j) = 1
                end if
            end if
        end do
        is_prerequisite_satisfiable_in_curriculum = tmpPreq(1)>0
        return
    end function is_prerequisite_satisfiable_in_curriculum


    function text_curriculum_info(idxCURR)
        integer, intent (in) :: idxCURR
        character (len=MAX_LEN_CURRICULUM_CODE+MAX_LEN_CURRICULUM_NAME+MAX_LEN_CURRICULUM_NAME+MAX_LEN_CURRICULUM_NAME) :: tmp, &
        text_curriculum_info
        tmp = SPACE
        if (Curriculum(idxCURR)%Remark/=SPACE)  &
        tmp = COMMA//SPACE//trim(Curriculum(idxCURR)%Remark)//tmp
        if (Curriculum(idxCURR)%Specialization/=SPACE)  &
        tmp = COMMA//SPACE//trim(Curriculum(idxCURR)%Specialization)//tmp
        tmp = trim(Curriculum(idxCURR)%Code)//SPACE//DASH//SPACE// &
        trim(Curriculum(idxCURR)%Title)//tmp
        text_curriculum_info = tmp
        return
    end function text_curriculum_info


    subroutine xml_write_intake(path)

        character(len=*), intent(in) :: path
        integer :: idxCURR

        ! training only?
        if (noWrites) return

        fileName = trim(dirXML)//trim(path)//'INTAKE.XML'

        call xml_open_file(unitNo, XML_INTAKE, fileName, idxCURR)

        write(unitNo,AFORMAT) &
        '    <comment>', &
        '        Generated by '//PROGNAME//VERSION//' on '//currentDate(1:4)//FSLASH// &
                    currentDate(5:6)//FSLASH//currentDate(7:8), &
        '        Intake - curriculum,count', &
        '    </comment>'

        do idxCURR=1,NumCurricula
            if (NFintake(idxCURR)==0) cycle
            call xml_write_character(unitNo, indent0, 'Intake', &
                trim(Curriculum(idxCURR)%Code)//COMMA//itoa(NFintake(idxCURR)) )
        end do

        call xml_close_file(unitNo, XML_INTAKE)

        return
    end subroutine xml_write_intake


    subroutine xml_read_intake(path, errNo)

        character(len=*), intent(in) :: path
        integer, intent (out) :: errNo

        integer :: idxCURR
        character (len=MAX_LEN_CURRICULUM_CODE) :: tCurriculum
        character(len=MAX_LEN_XML_LINE) :: value
        character(len=MAX_LEN_XML_TAG) :: tag

        ! open file, return on any error
        fileName = trim(dirXML)//trim(path)//'INTAKE.XML'
        call xml_open_file(unitNo, XML_INTAKE, fileName, errNo, forReading)
        if (errNo/=0) return

        ! examine the file line by line
        do
            read(unitNo, AFORMAT, iostat=eof) line
            if (eof<0) exit

            ! get tag and value if any; exit on any error
            call xml_parse_line(line, tag, value, eof)
            if (eof/=0) exit

            select case (trim(tag))

                case ('Intake')
                    call index_to_delimiters(COMMA, value, ndels, pos)
                    tCurriculum = value(:pos(2)-1)
                    idxCURR = index_to_curriculum(tCurriculum)
                    if (idxCURR == 0) then
                        cycle
                    else if (idxCURR <= 0) then
                        idxCURR = -idxCURR
                    end if
                    NFintake(idxCURR) = atoi(value(pos(2)+1:pos(3)-1))

                case default ! do nothing

            end select

        end do

        call xml_close_file(unitNo)

        return
    end subroutine xml_read_intake


end module CURRICULA
